/*************************************************************************
    > File Name: hello_word.c
    > Author: Xing.Dai
    > Mail: Xing.Dai@aliyun.com 
    > Created Time: 2016-11-06 06:18
 ************************************************************************/
//#include <linux/config.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>

#include <linux/kernel.h>	/* printk() */
#include <linux/slab.h>		/* kmalloc() */
#include <linux/fs.h>		/* everything... */
#include <linux/errno.h>	/* error codes */
#include <linux/types.h>	/* size_t */
#include <linux/proc_fs.h>
#include <linux/fcntl.h>	/* O_ACCMODE */
#include <linux/seq_file.h>
#include <linux/cdev.h>

//#include <asm/system.h>		/* cli(), *_flags */
#include <linux/uaccess.h>	/* copy_*_user */

#include "scull.h"		/* local definitions */

int scull_major = SCULL_MAJOR;
int scull_minor = 0;
int scull_nr_devs = SCULL_NR_DEVS;

struct scull_dev *scull_devices;
int scull_quantum = SCULL_QUANTUM;
int scull_qset = SCULL_QSET;

module_param(scull_major, int, S_IRUGO);
module_param(scull_minor, int, S_IRUGO);
module_param(scull_nr_devs, int, S_IRUGO);
module_param(scull_quantum, int, S_IRUGO);
module_param(scull_qset, int, S_IRUGO);

MODULE_AUTHOR("xing.dai <xing.dai@aliyun.com>");
MODULE_LICENSE("Dual BSD/GPL");

/**
 * @Synopsis  
 *
 * @Param dev
 * @Param n
 *
 * @Returns   
 */
struct scull_qset *scull_follow(struct scull_dev *dev, int n)
{
	struct scull_qset *qs = dev->data;

	if(!qs) {
		/* warninig: need assigned to dev->data! qs is a temp pointer. */
		qs = dev->data = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
		if(qs == NULL)
			return NULL;
		memset(qs, 0, sizeof(struct scull_qset));
	}

	while(n--) {
		if(!qs->next) {
			qs->next = kmalloc(sizeof(struct scull_qset), GFP_KERNEL);
			if(qs->next == NULL) 
				return NULL;
			memset(qs->next, 0, sizeof(struct scull_qset));
		}
		qs = qs->next;
		continue;
	}
	return qs;
}

/**
 * @Synopsis  
 *
 * @Param filp
 * @Param buf
 * @Param count
 * @Param f_pos
 *
 * @Returns   
 */
ssize_t scull_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{	
	struct scull_dev *dev = filp->private_data;
	struct scull_qset *dptr;
	int quantum = dev->quantum;
	int qset = dev->qset;
	int itemsize = quantum * qset;
	int item, s_pos, q_pos, rest;
	ssize_t retval = 0;


	PDEBUG("%s: begin read , f_pos=%d, count=%d.\n", __func__, (int)*f_pos, (int)count);
	if(down_interruptible(&dev->sem))
		return -ERESTARTSYS;
	if(*f_pos >= dev->size)
		goto out;
	if(*f_pos + count > dev->size) 
		count = dev->size - *f_pos;

	/* find list item, qset index, and offset in the quantum. */
	item = (long)*f_pos / itemsize;
	rest = (long)*f_pos % itemsize;
	s_pos = rest / quantum;
	q_pos = rest % quantum;
	dptr = scull_follow(dev, item);
	if(dptr == NULL || !dptr->data || !dptr->data[s_pos]) 
		goto out;	/* do not fill holes. */

	/* read only up to the end of this quantum. */
	if(copy_to_user(buf, dptr->data[s_pos] + q_pos, count)) {
		retval = -EFAULT;
		goto out;
	}
	PDEBUG("%s: before read , f_pos=%d, count=%d.\n", __func__, (int)*f_pos, (int)count);
	*f_pos += count;
	PDEBUG("%s: after  read , f_pos=%d, count=%d.\n", __func__, (int)*f_pos, (int)count);
	retval = count;

out:
	up(&dev->sem);
	return retval;
}


/**
 * @Synopsis  
 *
 * @Param filp
 * @Param buf
 * @Param size
 * @Param ppos
 *
 * @Returns   
 */
ssize_t scull_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
	struct scull_dev *dev = filp->private_data;
	struct scull_qset *dptr;
	int quantum = dev->quantum;
	int qset = dev->qset;
	int itemsize = quantum * qset;
	int item, s_pos, q_pos, rest;
	ssize_t retval = -ENOMEM;	/* value used in "goto out" statements. */

	if(down_interruptible(&dev->sem))
		return -ERESTARTSYS;

	/* find list item, qset index and offset in the quantum */
	item = (long)*f_pos / itemsize;
	rest = (long)*f_pos % itemsize;
	s_pos = rest / quantum;
	q_pos = rest % quantum;

	dptr = scull_follow(dev, item);
	if(dptr == NULL)
		goto out;
	if(!dptr->data) {
		dptr->data = kmalloc(qset * sizeof(char *), GFP_KERNEL);
		if(!dptr->data)
			goto out;
		memset(dptr->data, 0, qset * sizeof(char *));
	}
	if(!dptr->data[s_pos]) {
		dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
		if(!dptr->data[s_pos])
			goto out;
	}
	
	if(count > quantum - q_pos)
		count = quantum - q_pos;

	if(copy_from_user(dptr->data[s_pos] + q_pos, buf, count)) {
		retval = -EFAULT;
		goto out;
	}
	PDEBUG("%s: before write , f_pos=%d, count=%d.\n", __func__, (int)*f_pos, (int)count);
	*f_pos += count;
	PDEBUG("%s: after  write , f_pos=%d, count=%d.\n", __func__, (int)*f_pos, (int)count);
	retval = count;

	if(dev->size < *f_pos)
		dev->size = *f_pos;

out:
	up(&dev->sem);
	return retval;

}

/**
 * @Synopsis  
 *
 * @Param dev
 */
static int scull_trim(struct scull_dev *dev)
{
	struct scull_qset *next, *dptr;
	int qset = dev->qset;
	int i;

	for(dptr = dev->data; dptr; dptr = next) {
		/* all the list item*/
		if (dptr->data) {
			for(i = 0; i < qset; i++)
				kfree(dptr->data[i]);
			kfree(dptr->data);
			dptr->data = NULL;
		}
		next = dptr->next;
		kfree(dptr);
	}
	dev->size = 0;
	dev->quantum = scull_quantum;
	dev->qset = scull_qset;
	dev->data = NULL;
	return 0;
}

/**
 * @Synopsis  Open scull driver 
 *
 * @Param inode
 * @Param filp
 *
 * @Returns   
 */
int scull_open(struct inode *inode, struct file *filp)
{
	struct scull_dev *dev;

	dev = container_of(inode->i_cdev, struct scull_dev, cdev);
	filp->private_data = dev;

	if((filp->f_flags & O_ACCMODE) == O_WRONLY) {
		if(down_interruptible(&dev->sem))
			return -ERESTARTSYS;
		scull_trim(dev);
		up(&dev->sem);
	}

	return 0;
}

/**
 * @Synopsis The "extended" operations -- only seek 
 *
 * @Param filp
 * @Param off
 * @Param whence
 *
 * @Returns   
 */
loff_t scull_llseek(struct file *filp, loff_t off, int whence)
{
	struct scull_dev *dev = filp->private_data;
	loff_t newpos;

	switch(whence) {
		case 0: /* SEEK_SET*/
			newpos = off;
			break;
		case 1: /* SEEK_CUR */
			newpos = filp->f_pos + off;
			break;
		case 2: /* SEEK_END*/
			newpos = dev->size + off;
			break;
		default: /* Can not happen */
			return -EINVAL;
	}
	
	if(newpos < 0)
		return -EINVAL;
	filp->f_pos = newpos;

	return newpos;
}

/**
 * @Synopsis  
 *
 * @Param inode
 * @Param filp
 * @Param cmd
 * @Param arg
 *
 * @Returns   
 */
int scull_ioctl(struct inode *inode, struct file *filp, 
				unsigned int cmd, unsigned long arg)
{
	int err = 0;
	int tmp;
	int retval = 0;

	if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC)
		return -ENOTTY;
	if (_IOC_NR(cmd) > SCULL_IOC_MAXNR)
		return -ENOTTY;
	/* 
	 * The direction is a bitmask, and CERIFY_WRITE catches R/W
	 * transfers. "Type" is user-oriented, while
	 * assess_ok is kernel-oriented, so the concept of "read" and
	 * "write" is reversed. 
	 */
	if (_IOC_DIR(cmd) & _IOC_READ)
		err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));
	else if (_IOC_DIR(cmd) & _IOC_WRITE)
		err = !access_ok(VERIFY_READ, (void __user *)arg, _IOC_SIZE(cmd));

	if(err)
		return -EFAULT;
	/*
	 * S means "Set" through a ptr,
	 * T means "Tell" directly with the argument value
	 * G means "Get": reply by setting through a pointer
	 * Q means "Query": response is on the return value
	 * X means "eXchange": switch G and S atomically
	 * H means "sHift": switch T and Q atomically
	 */
	switch(cmd) {
		case SCULL_IOCRESET:
			scull_quantum = SCULL_QUANTUM;
			scull_qset = SCULL_QSET;
			break;
		/* quantum control for ioctl. */	
		case SCULL_IOCSQUANTUM: /* Set: arg points to the value. */
			if(!capable(CAP_SYS_ADMIN))
				return -EPERM;
			retval = __get_user(scull_quantum, (int __user *)arg);
			break;
		case SCULL_IOCTQUANTUM: /* Tell: arg is the value. */
			if(!capable(CAP_SYS_ADMIN))
				return -EPERM;
			scull_quantum = arg;	
			break;
		case SCULL_IOCGQUANTUM: /* Get: arg is pointer to result. */
			retval = __put_user(scull_quantum, (int __user *)arg);
			break;
		case SCULL_IOCQQUANTUM: /* Query: return is (it's positive) */
			return scull_quantum;
		case SCULL_IOCXQUANTUM: /* eXchange: use arg as pointer. */
			if(!capable(CAP_SYS_ADMIN))
				return -EPERM;
			tmp = scull_quantum;
			retval = __get_user(scull_quantum, (int __user *)arg);
			if(retval == 0)
				retval = __put_user(tmp, (int __user *)arg);
			break;
		case SCULL_IOCHQUANTUM: /* sHift: like Tell + Query. */
			if(!capable(CAP_SYS_ADMIN))
				return -EPERM;
			tmp = scull_quantum;
			scull_quantum = arg;
			return tmp;
		case SCULL_IOCSQSET:
			if(!capable(CAP_SYS_ADMIN))
				return -EPERM;
			retval = __get_user(scull_qset, (int __user *)arg);
			break;
		case SCULL_IOCTQSET:
			if(!capable(CAP_SYS_ADMIN))
				return -EPERM;
			scull_qset = arg;
			break;
		case SCULL_IOCGQSET:
			retval = __put_user(scull_qset, (int __user *)arg);
			break;
		case SCULL_IOCQQSET:
			return scull_qset;
		case SCULL_IOCXQSET:
			if(!capable(CAP_SYS_ADMIN))
				return -EPERM;
			tmp = scull_qset;
			retval = __get_user(scull_qset, (int __user *)arg);
			if (retval == 0)
				retval = __put_user(tmp, (int __user *)arg);
			break;
		case SCULL_IOCHQSET: 
			if(!capable(CAP_SYS_ADMIN))
				return -EPERM;
			tmp = scull_qset;
			scull_qset = arg;
			return tmp;
			
			/* 
			 * The following wo changge the buffer size for scullpipe.
			 * the scullpipe device uses this sam ioctl method, just to
			 * write less code. Actually, it's the sam driver, isn't it?
			 */
		case SCULL_P_IOCTSIZE:
			scull_p_buffer = arg;
			break;

		case SCULL_P_IOCQSIZE:
			return scull_p_buffer;

		default:
			break;
	}

	return retval;
}

/**
 * @Synopsis  fop
 */
struct file_operations scull_fops = {
	.owner   = THIS_MODULE,
	.read    = scull_read,
	.write   = scull_write,
	.open    = scull_open,
	.llseek  = scull_llseek,
#if 0
	.llseek  = NULL,
	.read    = NULL,
	.write   = NULL,
	.ioctrl  = NULL,
	.open    = NULL,
	.release = NULL,
#endif
};




/**
 * @Synopsis  
 *
 * @Param dev
 * @Param index
 */
static void scull_setup_cdev(struct scull_dev *dev, int index)
{
	int err;
	int devno = MKDEV(scull_major, scull_minor + index);

	cdev_init(&dev->cdev, &scull_fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &scull_fops;
	err = cdev_add(&dev->cdev, devno, 1);

	if (err)
		printk(KERN_NOTICE "Error %d scull %d.", err, index);
}

/**
 * @Synopsis  
 *
 * @Returns   
 */
static int __init scull_init_module(void)
{
	int result, i;
	dev_t dev = 0;

	printk(KERN_NOTICE"scull init begin.\n");
	
	/* 1. Get device number, include major & minor. */
	if(scull_major) {
		dev = MKDEV(scull_major, scull_minor);
		/* register_chr_dev_region(dev_t from, unsigned count, const char *name); */
		result = register_chrdev_region(dev, scull_nr_devs, "scull");
	} else {
		/* alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name);*/
		result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs, "scull");
		scull_major = MAJOR(dev);
	}

	if(result < 0) {
		printk(KERN_WARNING "scull: can't get major! %d\n", scull_major);
		return result;
	} else {
		printk(KERN_WARNING "scull get major %d success.\n", scull_major);
	}

	scull_devices = (struct scull_dev *)kmalloc(sizeof(struct scull_dev) * scull_nr_devs, GFP_KERNEL);

	if (!scull_devices) {
		result = -ENOMEM;
		goto fail;
	}

	memset(scull_devices, 0x00, sizeof(struct scull_dev) * scull_nr_devs);

	for (i = 0; i < scull_nr_devs; i++) {
		scull_devices[i].quantum = scull_quantum;
		scull_devices[i].qset = scull_qset;
		//init_MUTEX(&scull_devices[i].sem);
		sema_init(&scull_devices[i].sem, 1);
		scull_setup_cdev(&scull_devices[i], i);
		printk(KERN_NOTICE "scull_dev[%d] addr: %p\n", i, &scull_devices[i]);
	}
		
	dev = MKDEV(scull_major, scull_minor + scull_nr_devs);
	dev += scull_p_init(dev);	/* major dev num is sys alloc, minor dev num: 4, 5, 6, 7 */
	return 0;

fail:
	return result;
}


/**
 * @Synopsis  
 */
static void scull_cleanup_module(void)
{
	int i;
	dev_t devno = MKDEV(scull_major, scull_minor);

	if (scull_devices) {
		for (i = 0; i < scull_nr_devs; i++) {
			scull_trim(&scull_devices[i]);
			cdev_del(&scull_devices[i].cdev);
		}
		kfree(scull_devices);
	}

	unregister_chrdev_region(devno, scull_nr_devs);

	/* and call the cleanupo function for friend devices. */
	scull_p_cleanup();

	printk(KERN_NOTICE "unload scull.\n");
}

module_init(scull_init_module);
module_exit(scull_cleanup_module);



